//
//  Renderer.cpp
//  RayTracer
//
//  Created by 俞云康 on 2/15/16.
//  Copyright © 2016 yushroom. All rights reserved.
//

#include "Renderer.hpp"
#include <thread>
#include <mutex>
#include "Timer.hpp"
#include "Params.hpp"

shared_ptr<Renderer> Renderer::create(const Params& params) {
	int camera_samples = params.getInt("cameraSamples");
	int light_samples = params.getInt("lightSamples");
	float report_interval = params.getFloat("reportInterval");
	return make_shared<Renderer>(camera_samples, light_samples, report_interval);
}


Renderer::Renderer(int		nCameraSamples,
				   int		nIntegratorSamples,
                   float    report_interval)
    :	m_nCameraSamples(nCameraSamples),
        m_nIntegratorSamples(nIntegratorSamples),
        report_interval(report_interval)
{
    
}

enum BlockMovement {
	BM_RIGHT = 0,
	BM_DOWN = 1,
	BM_LEFT = 2,
	BM_UP = 3,
    BM_NONE = 4, // only for first block
};

struct RenderBlock {
	int x, y, w, h;
};

void Renderer::render(const Scene&  scene,
                      const Camera& camera,
                      Canvas&       canvas,
                      shared_ptr<SurfaceIntegrator>
                                    surf_integrator)
{
    render_region(scene, camera, canvas, surf_integrator, 0, 0, 1, 1);
}


void splitRenderBlocks(const int w, const int h, const int block_size, vector<RenderBlock>* render_blocks) {
	Assert(render_blocks != nullptr);
	Assert(w > 0 && h > 0 && block_size > 0);
	const int block_count_x = ceil2Int((w / 2) / float(block_size)) * 2;
	const int block_count_y = ceil2Int((h / 2) / float(block_size)) * 2;
    //info("n_blocks_x = %d n_blocks_y = %d\n", block_count_x, block_count_y);
	const int n_blocks = block_count_x * block_count_y;
    const int max_moves = block_count_x > block_count_y ? block_count_x*block_count_x : block_count_y * block_count_y;
	//right->down->left->up
	vector<BlockMovement> block_movements;
	{
		block_movements.reserve(max_moves);
		int t = 1;
		int bm_counts = 0;
		int current_move = 0;
        block_movements.push_back(BM_NONE);
        bm_counts ++;
		while (bm_counts < max_moves) {
			for (int i = 0; i < t; ++i) {
				block_movements.push_back(BlockMovement(current_move));
			}
			current_move = (current_move + 1) % 4;
			for (int i = 0; i < t; ++i) {
				block_movements.push_back(BlockMovement(current_move));
			}
			current_move = (current_move + 1) % 4;
			bm_counts += t * 2;
			t++;
		}
	}

	int px = w / 2 - block_size;
	int py = h / 2 - block_size;
	int pw = block_size;
	int ph = block_size;
	render_blocks->clear();
	render_blocks->reserve(n_blocks);
    int move_idx = 0;
	for (int idx = 0; idx < n_blocks; ++idx) {
    RETRY:
        Assert(move_idx < max_moves);
		auto move = block_movements[move_idx++];
		switch (move) {
		case BM_LEFT:
			px -= block_size; break;
		case BM_RIGHT:
			px += block_size; break;
		case BM_UP:
			py -= block_size; break;
		case BM_DOWN:
			py += block_size; break;
        case BM_NONE: // first block
            break;
		default:
			error("unexpected BlockMovement\n"); break;
		}
		int ppx = px;
		int ppy = py;
		pw = ph = block_size;
        if (ppx <= -block_size || ppx >= w) {
            goto RETRY;
        } else if (ppx < 0) {
			pw = block_size + ppx;
            ppx = 0;
		} else if (ppx + block_size > w) {
			pw = w - ppx;
		}
        // else if (ppx < block_size) {
        //     pw = block_size + ppx;
        //     ppx = 0;
        // }

		if (ppy <= -block_size || ppy >= h) {
            goto RETRY;
        } else if (ppy < 0) {
			ph = block_size + ppy;
            ppy = 0;
		} else if (ppy + block_size > h) {
			ph = h - ppy;
		}
        // else if (ppy < block_size) {
        //     ph = block_size + ppy;
        //     ppy = 0;
        // }
		render_blocks->push_back(RenderBlock{ ppx, ppy, pw, ph });
	}
}


void Renderer::render_region(const Scene&     scene,
                             const Camera&    camera,
                             Canvas&          canvas,
                             shared_ptr<SurfaceIntegrator> surf_integrator,
                             float x, float y, float width, float height)
{
    this->scene = &scene;
    this->camera = &camera;
    this->surf_integrator = surf_integrator;
    mWidth = canvas.m_width;
    mHeight = canvas.m_height;
    p = canvas.pixels.data();
    ok = 0;
    const int w = round2Int(width * canvas.m_width);
    const int h = round2Int(height * canvas.m_height);
    const int all = w*h;
    //const int offset = int(x*canvas.width) + int(y*canvas.height)*canvas.width;
    ProgressReporter reporter("rendering", all, report_interval);
    Timer timer("Rendering");
    timer.begin();
    //#define	MULTI_THREAD
    const int xx = round2Int(x*mWidth);
    const int yy = round2Int(y*mHeight);

//#define MULTI_THREAD
#ifdef MULTI_THREAD
	const int block_size = 32;
	vector<RenderBlock> render_blocks;
	splitRenderBlocks(w, h, block_size, &render_blocks);

    const int num_thread = std::thread::hardware_concurrency();
    warning("Number of rendering threads: %d\n", num_thread);
    //const int num_thread = 4;
    vector<std::thread> threads;
    //std::thread t[num_thread];
    int block_index = 0;
    //int total_blocks = render_blocks.size();
    for (int i = 0; i < num_thread; ++i) {
        threads.emplace_back(std::thread([this, &block_index, &render_blocks, &reporter](){
            while (block_index < render_blocks.size()) {
                this->log_mutex.lock();
                //info("rendering block index = %d\n", block_index);
                auto& b = render_blocks[block_index++];
                this->log_mutex.unlock();
                this->render_paral(b.x, b.y, b.w, b.h, reporter);
            }
        }));
    }
    for (int i = 0; i < num_thread; ++i) {
     threads[i].join();
    }

//#ifdef MULTI_THREAD
//    const int num_thread = std::thread::hardware_concurrency;
//#if 1
//    std::thread t[num_thread];
//#elif 1
//    std::thread t[num_thread];
//    //const int step = all / num_thread;
//    const int half_w = round2Int(w/2.0f);
//    const int half_h = round2Int(h/2.0f);
//    t[0] = std::thread(&Renderer::render_paral, this, xx, yy, half_w, half_h, reporter);
//    t[1] = std::thread(&Renderer::render_paral, this, xx+half_w, yy, half_w, half_h, reporter);
//    t[2] = std::thread(&Renderer::render_paral, this, xx, yy+half_h, half_w, half_h, reporter);
//    t[3] = std::thread(&Renderer::render_paral, this, xx+half_w, yy+half_h, half_w, half_h, reporter);
//#else
//	std::thread t[num_thread];
//	const int h_step = round2Int(float(h) / num_thread);
//	for (int i = 1; i < num_thread; ++i) {
//		t[i-1] = std::thread(&Renderer::render_paral, this, xx, yy+h_step*(i-1), w, h_step, reporter);
//	}
//	t[num_thread-1] = std::thread(&Renderer::render_paral, this, xx, yy + h_step*(num_thread - 1), w, h-h_step*(num_thread-1), reporter);
//#endif
////    for (int i = 0; i < num_thread-1; ++i) {
////        t[i] = std::thread(&Renderer::render_paral, this, offset+step*i, offset+step*(i + 1), reporter);
////    }
////    t[num_thread - 1] = std::thread(&Renderer::render_paral, this, step*(num_thread - 1), all, reporter);
//    for (int i = 0; i < num_thread; ++i) {
//        t[i].join();
//    }
#else
    render_paral(xx, yy, w, h, reporter);
#endif
    timer.end_and_print();
}

// render subregion
void Renderer::render_paral(const int   x,
                            const int   y,
                            const int   w,
                            const int   h,
                            const ProgressReporter&
                                        reporter)
{
	//log_mutex.lock();
    //info("Renderer::render_paral x: %d, y: %d, w: %d, h: %d\n", x, y, w, h);
	//log_mutex.unlock();
    RNG rng;
    RandomSampler camera_sampler(m_nCameraSamples);
    RandomSampler integrator_sampler(m_nIntegratorSamples);
    float inv_w = 1.0f / mWidth;
    float inv_h = 1.0f / mHeight;
    //ProgressReporter reporter("rendering", idx_end-idx_begin, 0.02f);
    //int ok = 0;
    
//#pragma omp parallel for
    for (int i = x; i < x+w; ++i) {
        for (int j = y; j < y+h; ++j) {
            //int x = i % mWidth, y = i / mWidth;
            Color r(0, 0 ,0);
            for (int s = 0; s < camera_sampler.m_samplersPerPixel; ++s) {
                //auto& sa = sampler.samples[s];
                //Sample sa = camera_sampler.sample_unit_sqrare(rng);
                Sample sa = camera_sampler.get2D();
                // -0.5f, sa.x, sa.y are in range [0, 1] -> [-0.5, 0.5]
                // +0.5f, pixel offset
                // -0.5f, center of the screen is (0.5, 0.5)
                Ray ray = camera->ray_at(float(i+sa.x/*-0.5f+0.5f*/)*inv_w-0.5f, float(j+sa.y/*-0.5f+0.5f*/)*inv_h-0.5f);
                HitInfo hit;
                
                hit.in_dir = ray.dir;
                if (scene->intersect(ray, &hit)) {
                    r += surf_integrator->Li(*scene, *this, ray, hit, integrator_sampler, rng);
                }
            }
            p[i+j*mWidth] = r / (float)camera_sampler.m_samplersPerPixel;
            log_mutex.lock();
            reporter.update(++ok);
            log_mutex.unlock();
            //info("ok: %d\n", ok);
        }
    }
}

void Renderer::render_pixel(const Scene& scene,
                            const Camera& camera,
                            Canvas& canvas,
                            shared_ptr<SurfaceIntegrator> surf_integrator,
                            int x, int y)
{
    RNG rng;
    this->scene = &scene;
    this->camera = &camera;
    this->surf_integrator = surf_integrator;
//    JitterSampler integrator_sampler(sqrt_samples_ingerator, 83);
//    JitterSampler camera_sampler(sqrt_samples_camera);
    RandomSampler camera_sampler(m_nCameraSamples);
    RandomSampler integrator_sampler(m_nIntegratorSamples);
    mWidth = canvas.m_width;
    mHeight = canvas.m_height;
    float inv_w = 1.0f / mWidth;
    float inv_h = 1.0f / mHeight;
    
    for (int s = 0; s < camera_sampler.m_samplersPerPixel; ++s) {
        //Sample sa = camera_sampler.sample_unit_sqrare(rng);
        Sample sa = camera_sampler.get2D();
        Ray ray = camera.ray_at(float(x+sa.x/*-0.5f+0.5f*/)*inv_w-0.5f, float(y+sa.y/*-0.5f+0.5f*/)*inv_h-0.5f);
        HitInfo hit;
        Color r(0, 0 ,0);
        
        hit.in_dir = ray.dir;
        if (scene.intersect(ray, &hit)) {
            r += surf_integrator->Li(scene, *this, ray, hit, integrator_sampler, rng);
        }
    }
}

Color Renderer::Li(const Scene& scene,
                   const Ray&   ray,
                   Sampler&     sampler,
                   RNG&         rng) const
{
    Color T;
    HitInfo hit;
    Color Li(0, 0, 0);
    if (scene.intersect(ray, &hit)) {
        Li = surf_integrator->Li(scene, *this, ray, hit, sampler, rng);
    }
    return Li;
}
